package com.lzh.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.lzh.gulimall.product.service.CategoryBrandRelationService;
import com.lzh.gulimall.product.vo.Catelog2Vo;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.nio.channels.ScatteringByteChannel;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lzh.common.utils.PageUtils;
import com.lzh.common.utils.Query;

import com.lzh.gulimall.product.dao.CategoryDao;
import com.lzh.gulimall.product.entity.CategoryEntity;
import com.lzh.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装成父子的树形结构
        //获取一级分类，递归获取子分类并设置，根据排序字段进行排序
        List<CategoryEntity> level1Menus = entities.stream()
                .filter(e -> e.getParentCid() == 0)
                .map(menu -> {
                    menu.setChildren(getChildrens(menu, entities));
                    return menu;
                })
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());
        return level1Menus;
    }

    @Override
    public void removeMenusByIds(List<Long> asList) {
        //todo 检查当前删除的菜单，是否被别的地方引用

        //批量删除
        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCategoryPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);

        Collections.reverse(parentPath);


        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 级联更新所有关联的数据
     *
     * @param category
     */
    //失效模式的使用
//    @CacheEvict(value = "category",key = "'getLevel1Categorys'")
    //同时进行多种缓存操作
    @Caching(evict ={
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    } )
    //allEntries = true指定删除category分区内的所有数据
//    @CacheEvict(value = "category",allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    //[2,25,225]
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);

        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 获取以及分类(首页用)
     * @return
     */
    //代表当前方法的结果需要缓存，如果缓存中有，方法不用调用。
    //如果缓存中没有，会调用方法，最后将方法的结果放入缓存
    //这一个注解就相当于读模式
    //每一个需要缓存的数据我们都来指定要放到哪个名字的缓存[缓存的分区(按照业务类型分)]
    //value表示分区，key表示缓存的name,可以使用spel表达式
    @Cacheable(value = "category",key = "#root.method.name",sync =true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        long start = System.currentTimeMillis();
        List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        long end = System.currentTimeMillis();

        System.out.println("消耗时间：" + (end - start));
        return entities;
    }

    @Cacheable(value = "category",key = "#root.method.name")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        System.out.println("查询了数据库...");

        //直接出所有分类
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //封装数据
        Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //每一个一级分类，查其二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            //封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    //二级分类
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //找到当前二级分类的三级分类封装成vo
                    List<CategoryEntity> categoryEntities1 = getParent_cid(selectList, l2.getCatId());
                    if (categoryEntities1 != null) {
                        List<Catelog2Vo.Category3Vo> collect = categoryEntities1.stream().map(l3 -> {
                            //三级分类
                            Catelog2Vo.Category3Vo category3Vo =
                                    new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return category3Vo;
                        }).collect(Collectors.toList());
                        //将三级分类封装到二级分类中
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());

            }
            return catelog2Vos;
        }));
        return parentCid;
    }

        /**
         * springboot2.0以后，默认使用letture作为操作redis的客户端。它使用netty进行网络通讯
         * letture的bug导致netty直接内存溢出
         *
         * SpringCache
         * 给每一个需要缓存的业务加上缓存的使用逻辑。
         * 缓存的使用，就得考虑缓存的两种用法模式：
         *      读模式:
         *          先从缓存中读取，缓存没有，在从数据库中读取，并把数据放到缓存中，然后返回数据
         *      写模式：
         *          如何保证缓存与数据库中的数据是一致的？可以使用双写模式或者失效模式
         *          双写模式：写完数据库，缓存跟着改一下
         *          失效模式: 写完数据库，对应的缓存删了，等待下次主动查询，更新缓存即可
         *          脏数据的问题，可以通过加读写锁解决
         *如果每一个需要使用缓存的业务代码，都是这种编码模式，每次都这样写，要麻烦了，希望有一个简单
         *的方式整合使用缓存。
         * spring专门抽取一个叫SpringCache的框架，是对缓存的一个抽象层
         *
         */
//    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
        /**
         * 1.空结果缓存：解决缓存穿透。
         * 2.设置过期时间(加随机值): 解决缓存雪崩的问题
         * 3.加锁：解决缓存击穿问题
         *      本地锁只能锁住当前进程，我们需要分布式锁
         */

        //加入缓存逻辑，缓存中存的数据是json字符串
        //json跨语言，跨平台兼容
        String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");

        //缓存为空
        if (StringUtils.isEmpty(catalogJson)) {
            //缓存中没有，就查询数据库
            //***加锁：解决缓存击穿问题
            //本地锁: synchronized、JUC(Lock),分布式情况下，想要锁住所有，必须使用分布式锁
            System.out.println("缓存没命中，查询数据库");
//            Map<String, List<Catelog2Vo>>  catalogJsonFromDb= getCatalogJsonFromDb();
            Map<String, List<Catelog2Vo>>  catalogJsonFromDb= getCatalogJsonFromDbWithRedisLock();
            //***查询菜单这个业务，不存在缓存穿透的问题，因为查询菜单不需要入参，所以不需要将空结果缓存
            return catalogJsonFromDb;
        }

        System.out.println("缓存命中，直接返回");
        //缓存不为空，把json数据转为对象
        Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });

        return stringListMap;
    }

    /**
     * 分布式锁使用Redission实现
     * 缓存里的数据如何和数据库的数据保持一致？？
     * 缓存数据一致性
     * 1)、双写模式:写完数据库，缓存跟着改一下
     *      缺点：线程1改完数据库，准备去改缓存，结果没cpu时间片了或者执行的慢，此时线程2快速的改数据库，改缓存，
     *           然后线程1才把缓存改了。这就出现了脏数据了，本来线程2的修改数据才是最新的数据，结果因为线程1的
     *           原因，出现了脏数据。
     *      解决：
     *            1.改数据和改缓存，这两部操作，用锁锁起来
     *            2.如果能容忍最终一致性，可以给缓存设置一个过期时间。也是ok的
     * 2)、失效模式: 写完数据库，对应的缓存删了，等待下次主动查询，更新缓存即可
     *      缺点：请求1在一号机器，写完数据，并把缓存删了；
     *           请求2进来二号机器，改数据库1号数据改为2，但是机器2执行的慢，还没改完，
     *           此时请求3进来，发现确实没缓存，读取数据库，读取到了老的数据(机器2还没改完数据)，
     *           然后要把读取到的老数据放到缓存里，但是机器三把数据放到缓存的操作比较慢，机器二
     *           在这段时间改完了数据，也删了缓存，然后机器三把数据放到了缓存里，此时，缓存里的数据
     *           就不是最新的数据了
     *      解决：
     *            1.改数据和改缓存，这两部操作，用锁锁起来。但是加锁是很笨重的
     *                  如果数据经常修改，还要不要放缓存，为了保证缓存数据一致性，经常加锁，整个系统就很慢，
     *                  还不如直接查询数据库，不放缓存了
     *                  因此，经常修改的数据，想要实时的读取，实时性要求高的，直接读数据库
     *
     *缓存数据一致性-解决方案
     *无论是双写模式还是失效模式，都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办？
     *1、如果是用户纬度数据（订单数据、用户数据），这种并发几率非常小，不用考虑这个问题，缓存数据加上过期时间，每隔一段时间触发读的主动更新即可
     *2、如果是菜单，商品介绍等基础数据，这些数据是能容忍一定程度的缓存不一致，比如：ipone手机的介绍，在后台修改了，过一段时间，页面才同步过来，
     *   虽然是最终一致性，但是对我们的业务并没有什么影响，这些数据能容忍缓存的不一致。如果想要实时的一致性，也可以去使用canal订阅binlog的方式。
     *3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
     *4、通过加锁保证并发读写，写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。（业务不关心 脏数据，允许临时脏数据可忽略）；
     *
     * 总结：
     * 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间，保证每天拿到当前最新数据即可。
     * 害怕脏数据，顶多加个读写锁
     * 遇到实时性、一致性要求高的数据，就应该直接查数据库，即使慢点。
     * 还可以使用canal完美的解决缓存一致性的问题。
     *
     * 我们系统的一致性解决方案：
     * 1、缓存的所有数据都有过期时间，数据过期下一次查询触发主动更新
     * 2、读写数据的时候，加上分布式的读写锁。 经常写，经常读，会对系统性能有极大的影响
     *
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        //1、占分布式锁。去redis占坑
        //（锁的粒度，越细越快:具体缓存的是某个数据，11号商品） product-11-lock
        RLock  rLock= redissonClient.getLock("catalogJson-lock");
        //创建读锁
//        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
//        RLock rLock = readWriteLock.readLock();

        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            rLock.lock();
            //加锁成功...执行业务
            dataFromDb = getDataFromDb();
        } finally {
            rLock.unlock();
        }
        //先去redis查询下保证当前的锁是自己的
        //获取值对比，对比成功删除=原子性 lua脚本解锁
        // String lockValue = stringRedisTemplate.opsForValue().get("lock");
        // if (uuid.equals(lockValue)) {
        //     //删除我自己的锁
        //     stringRedisTemplate.delete("lock");
        // }

        return dataFromDb;

    }

    /**
     * 自己实现redis分布式锁
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //占分布式锁。去redis占锁
        String uuid = UUID.randomUUID().toString();
        //setIfAbsent如果键不存在则新增,存在则不改变已经有的值。
        //设置值和超时时间，保证加锁是原子性的
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);

        //上锁成功
        if (lock){
            System.out.println("获取分布式锁成功...");
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                //执行业务逻辑
                dataFromDb= getDataFromDb();
            }finally {
                //lua脚本解锁
                String script="if redis.call('get',KEYS[1]) == ARGV[1] then \n" +
                        "   return redis.call('del',KEYS[1]) \n" +
                        "else\n" +
                        "   return 0 \n" +
                        "end";
                //删除锁，保证原子性
                Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else{
            System.out.println("获取分布式锁失败...等待重试");
            //加锁失败...重试
            //休眠
            try {
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
            //自旋锁
            return getCatalogJsonFromDbWithRedisLock();
        }
    }

    /**
     * 本地锁
     * @return
     */
    //从数据库查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
        /**
         * 原本的业务逻辑：
         * 先查出所有的一级分类（这里与db进行一次交互）
         * 然后遍历每一个一级分类,再查它的二级分类（这里与db进行多次交互）
         * 然后再根据二级分类查三级分类（这里与db进行多次交互）
         * 这就造成了多次与数据库交互，特别浪费性能
         *
         * 优化业务逻辑：
         * 将多次数据库的查询变为一次。
         * 直接一次性查出所有的分类（这里与db进行一次交互）
         * 然后使用编写一个方法A，根据父级分类id，过滤出当前级别的分类，使用stream流实现
         * 比如：
         * 过滤出一级分类，一级分类的父级id为0，直接过滤书一级分类的list集合
         * 然后再遍历这个一级分类，再调用A,传当前一级分类的id进去，就能过滤出二级分类集合
         * 同理，三级分类也一样
         * 把查出的这些数据，封装到出参里即可
         *
         */
        synchronized (this) {
            return getDataFromDb();
        }
    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //当前线程获取到锁后，应该在确认一下，之前是不是已经有线程获取到锁，查询db并把缓存已经设置上了
        ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
        String catalogJson = stringStringValueOperations.get("catalogJson");
        //已经有缓存了
        if (!StringUtils.isEmpty(catalogJson)) {
            //缓存不为空，把json数据转为对象
            Map<String, List<Catelog2Vo>> stringListMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return stringListMap;
        }
        System.out.println("查询了数据库...");

        //直接出所有分类
        List<CategoryEntity> selectList = baseMapper.selectList(null);

        //查出所有一级分类
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //封装数据
        Map<String, List<Catelog2Vo>> parentCid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //每一个一级分类，查其二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            //封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    //二级分类
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //找到当前二级分类的三级分类封装成vo
                    List<CategoryEntity> categoryEntities1 = getParent_cid(selectList, l2.getCatId());
                    if (categoryEntities1 != null) {
                        List<Catelog2Vo.Category3Vo> collect = categoryEntities1.stream().map(l3 -> {
                            //三级分类
                            Catelog2Vo.Category3Vo category3Vo =
                                    new Catelog2Vo.Category3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return category3Vo;
                        }).collect(Collectors.toList());
                        //将三级分类封装到二级分类中
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());

            }
            return catelog2Vos;
        }));

        //查询到数据，再放入缓存，先将对象转为json
        String string = JSON.toJSONString(parentCid);
        //***放入缓存,并设置随机的过期时间，解决缓存雪崩的问题
        Random random = new Random();
        int timeout = random.nextInt(9) + 1;
        stringStringValueOperations.set("catalogJson", string, timeout, TimeUnit.HOURS);

        return parentCid;
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().longValue() == parent_cid.longValue()).collect(Collectors.toList());
        return collect;
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        //1、收集当前节点id
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;

    }

    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
        Long catId = root.getCatId();
        List<CategoryEntity> child = all.stream()
                //获取子级
                .filter(level1Menu -> level1Menu.getParentCid().longValue() == catId.longValue())
                //给当前分类设置子级，递归设置
                .map(categoryEntity -> {
                    categoryEntity.setChildren(getChildrens(categoryEntity, all));
                    return categoryEntity;
                })
                //排序
                .sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
                .collect(Collectors.toList());

//        if (child==null){
//            return child;
//        }else {
//            child.forEach(c->{
//                c.setChildren(getChildrens(c,all));
//            });
//        }
        return child;
    }

}